<template>
  <div v-if="showEmptyState" class="m-auto mt-8 max-w-5xl">
    <IEmptyState v-bind="emptyState" />
  </div>

  <IOverlay :show="!initialDataLoaded && !showEmptyState">
    <div v-show="!showEmptyState && initialDataLoaded">
      <div class="mb-2 flex flex-wrap items-center md:mb-4 md:flex-nowrap">
        <div
          class="order-last w-full shrink-0 md:order-first md:mb-0 md:w-auto"
        >
          <SearchInput
            v-model="search"
            :input-id="`${resourceName}-table-search`"
            @input="request(true)"
          />
        </div>

        <!-- <TablePerPageOptions
          v-model:per-page="collection.perPage"
          :per-page-options="collection.perPageOptions"
          class="ml-4"
        /> -->

        <div class="flex grow">
          <slot name="after-search" :collection="collection"></slot>
        </div>

        <div>
          <TableSettings
            v-if="componentReady"
            ref="settingsRef"
            :customizeable="tableSettings.customizeable"
            :with-customize-button="withCustomizeButton"
            :url-path="computedUrlPath"
            :table-id="tableId"
            :resource-name="resourceName"
          />
        </div>
      </div>

      <div
        v-if="componentReady && hasRules"
        class="mb-2 flex flex-col items-start md:mb-4 md:flex-row"
      >
        <div
          class="mb-2 flex w-full shrink-0 content-center space-x-1 sm:w-auto md:mb-0"
        >
          <FiltersDropdown
            :view="filtersView"
            :identifier="filtersIdentifier"
            class="flex-1"
            placement="bottom-start"
            @apply="applyFilters"
          />

          <IButton
            v-show="(hasRulesApplied || activeFilter) && !filtersBuilderVisible"
            variant="white"
            icon="PencilAlt"
            @click="toggleFiltersBuilderVisibility"
          />

          <IButton
            v-show="!hasRulesApplied && !filtersBuilderVisible && !activeFilter"
            variant="white"
            icon="Plus"
            :text="$t('core::filters.add_filter')"
            @click="toggleFiltersBuilderVisibility"
          />
        </div>

        <div class="mb-2 ml-0 md:mb-0 md:ml-4">
          <RulesDisplay :identifier="filtersIdentifier" :view="filtersView" />
        </div>
      </div>

      <FilterBuilder
        v-if="componentReady"
        :view="filtersView"
        :identifier="filtersIdentifier"
        :active-filter-id="filterId"
        @apply="applyFilters"
      />

      <ICard no-body :overlay="isLoading">
        <div
          v-if="isSelectable && hasSelectedRows"
          class="fixed left-auto right-1 z-50 w-full max-w-xs rounded-md border border-neutral-600 bg-neutral-800 px-6 py-5 shadow-lg shadow-primary-400/40 dark:shadow-primary-900/40 md:left-80 md:right-auto md:max-w-md"
          :class="collection.total <= 5 ? 'bottom-20' : 'bottom-[3.3rem]'"
        >
          <div class="flex flex-col md:flex-row md:items-center">
            <div class="mr-8 flex shrink-0 items-center">
              <IFormCheckbox
                :id="`${tableId}ToggleAll`"
                :checked="hasSelectedRows"
                @change="unselectAllRows"
              />

              <label
                class="ml-1 text-sm text-neutral-100"
                :for="`${tableId}ToggleAll`"
                v-text="
                  $t('core::actions.records_count', {
                    count: selectedRowsCount,
                  })
                "
              />
            </div>

            <div class="md:w-68 mt-3 w-full md:mt-0">
              <ActionSelector
                type="select"
                size="sm"
                view="index"
                :ids="selectedRowsIds"
                :action-request-params="actionRequestParams"
                :actions="tableSettings.actions || []"
                :resource-name="resourceName"
                @run="$emit('actionExecuted', $event)"
              />
            </div>
          </div>
        </div>
        <!-- When no maxHeight is provided, just set the maxHeight to big number e.q. 10000px
                    because when the user previous had height, and updated resetted the table, VueJS won't set the height to auto
                    or remove the previous height -->
        <ITable
          v-show="componentReady"
          :id="'table-' + tableId"
          :sticky="tableSettings.maxHeight !== null"
          :condensed="tableSettings.condensed"
          class="rounded-lg"
          wrapper-class="-mt-px"
          :max-height="
            tableSettings.maxHeight !== null
              ? tableSettings.maxHeight + 'px'
              : '10000px'
          "
        >
          <thead>
            <SortableDraggable
              v-model="visibleColumns"
              :disabled="!tableSettings.customizeable"
              :move="e => $refs.settingsRef.customizationRef.onColumnMove(e)"
              tag="tr"
              :item-key="item => 'th' + item.attribute"
              v-bind="{ ...$draggable.scrollable, delay: 50 }"
            >
              <template #item="{ element, index }">
                <HeaderCell
                  :attribute="element.attribute"
                  :label="element.label"
                  :th-class="element.thClass"
                  :condensed="tableSettings.condensed"
                  :is-selectable="isSelectable && index === 0"
                  :is-sortable="element.sortable"
                  :is-primary="element.primary"
                  :is-centered="element.centered"
                  :is-ordered="collection.isOrderedBy(element.attribute)"
                  :is-sorted-ascending="
                    collection.isSorted('asc', element.attribute)
                  "
                  :width="element.width || 'auto'"
                  :min-width="element.minWidth"
                  :all-rows-selected="
                    isSelectable && index === 0 ? allRowsSelected : undefined
                  "
                  @sort-requested="attr => collection.toggleSortable(attr)"
                  @toggle-select-all="toggleSelectAll"
                />
              </template>
            </SortableDraggable>
          </thead>

          <tbody>
            <tr
              v-for="row in collection.items"
              :key="row.id"
              :class="[
                'group/tr',
                row._row_class,
                row.tSelected && '!bg-neutral-50 dark:!bg-neutral-800',
              ]"
              @click="selectOnRowClick($event, row)"
            >
              <DataCell
                v-for="(column, cidx) in visibleColumns"
                :key="'td-' + column.attribute"
                :attribute="column.attribute"
                :condensed="tableSettings.condensed"
                :has-required-field="
                  column.field ? column.field.isRequired : false
                "
                :newlineable="column.newlineable"
                :width="column.width || undefined"
                :min-width="column.minWidth"
                :link="column.link"
                :route="column.route"
                :td-class="column.tdClass"
                :is-primary="column.primary"
                :is-selected="row.tSelected || false"
                :is-centered="column.centered || false"
                :is-sortable="column.sortable || false"
                :is-selectable="isSelectable && cidx === 0"
                :row="row"
                @selected="onRowSelected"
              >
                <slot
                  v-bind="{ column, row, resourceName, resourceId: row.id }"
                  :name="column.attribute"
                >
                  <component
                    :is="
                      column.component || column.field?.indexComponent || 'span'
                    "
                    :field="
                      column.field
                        ? {
                            ...column.field,
                            value: row[column.attribute],
                          }
                        : undefined
                    "
                    v-bind="{ column, row, resourceName, resourceId: row.id }"
                    @reload="request"
                  />
                </slot>
              </DataCell>
            </tr>

            <tr v-if="!collection.hasItems">
              <td
                v-show="!isLoading && initialDataLoaded"
                class="is-empty"
                :colspan="totalColumns"
                v-text="emptyText"
              />
            </tr>
          </tbody>
        </ITable>
      </ICard>

      <TablePagination
        v-if="collection.hasPagination"
        class="mt-4 sm:mt-6"
        :is-current-page-check="page => collection.isCurrentPage(page)"
        :has-next-page="collection.hasNextPage"
        :has-previous-page="collection.hasPreviousPage"
        :links="collection.pagination"
        :render-links="collection.shouldRenderLinks"
        :from="collection.from"
        :to="collection.to"
        :total="collection.total"
        :loading="isLoading"
        size="sm"
        @go-to-next="collection.nextPage()"
        @go-to-previous="collection.previousPage()"
        @go-to-page="collection.page($event)"
      />
    </div>
  </IOverlay>
</template>

<script setup>
import {
  computed,
  nextTick,
  onBeforeUnmount,
  onMounted,
  onUnmounted,
  reactive,
  ref,
  watch,
} from 'vue'
import { useI18n } from 'vue-i18n'
import { useTimeoutFn } from '@vueuse/core'
import isEqual from 'lodash/isEqual'
import sortBy from 'lodash/sortBy'

import ActionSelector from '@/Core/components/Actions/ActionSelector.vue'
import FilterBuilder from '@/Core/components/Filters/FilterBuilder.vue'
import FiltersDropdown from '@/Core/components/Filters/FiltersDropdown.vue'
import RulesDisplay from '@/Core/components/QueryBuilder/RulesDisplay.vue'
import { useFilterable } from '@/Core/composables/useFilterable'
import { useGlobalEventListener } from '@/Core/composables/useGlobalEventListener'
import { useLoader } from '@/Core/composables/useLoader'
import { useQueryBuilder } from '@/Core/composables/useQueryBuilder'
import { CancelToken } from '@/Core/services/HTTP'

import { useTable } from '../../composables/useTable'
import Collection from '../Table/Collection'
import TablePagination from '../Table/TablePagination.vue'

// import TablePerPageOptions from './TablePerPageOptions.vue'
import DataCell from './ResourceTableDataCell.vue'
import HeaderCell from './ResourceTableHeaderCell.vue'
import TableSettings from './ResourceTableSettings.vue'

const props = defineProps({
  tableId: { type: String, required: true },
  resourceName: { type: String, required: true },
  actionRequestParams: {
    type: Object,
    default() {
      return {}
    },
  },
  dataRequestQueryString: {
    type: Object,
    default() {
      return {}
    },
  },
  withCustomizeButton: Boolean,
  emptyState: Object,
  urlPath: String,
  /**
   * The filter id to intially apply to the table
   * If not provided, the default one will be used (if any)
   */
  filterId: Number,
})

const emit = defineEmits(['loaded', 'actionExecuted'])

const { t } = useI18n()

let clearPollingIntervalId = null

const { setLoading, isLoading } = useLoader()

const {
  settings: tableSettings,
  fetchActions,
  fetchSettings,
} = useTable(props.tableId)

const collection = reactive(new Collection())
const search = ref('')
const componentReady = ref(false)
const watchersInitialized = ref(false)
const initialDataLoaded = ref(false)
const settingsRef = ref(null)

let requestCancelToken = null

const emptyText = computed(() => {
  if (collection.hasItems) return ''
  if (isLoading.value) return '...'
  if (search.value) return t('core::app.no_search_results')

  return t('core::table.empty')
})

const filtersIdentifier = computed(() => tableSettings.value.identifier)

const filtersView = computed(() => props.tableId)

const isEmpty = computed(() => collection.state.meta.pre_total === 0)

const showEmptyState = computed(() => {
  // Indicates whether there is performed any request to the server for data
  if (typeof collection.state.meta.pre_total == 'undefined') {
    return false
  }

  return isEmpty.value && props.emptyState != undefined
})

const requestQueryStringParams = computed(() => ({
  ...collection.urlParams,
  ...tableSettings.value.requestQueryString, // Additional server params passed from table php file
  ...props.dataRequestQueryString,
}))

// Ensure they are ordered by order so they are immediately updated when dragging the headings
const columns = computed(() =>
  sortBy(tableSettings.value.columns || [], 'order')
)

const visibleColumns = computed({
  get() {
    return columns.value.filter(
      column => (!column.hidden || column.hidden == false) && column.attribute
    )
  },
  set(value) {
    // We will make sure to update the store before the "save" request
    // so the changes are reflected on the ui immediately without
    // the user to wait the "save" request to finish.
    const currentColumns = tableSettings.value.columns.map(col => {
      const newValueColumn = value.find(vc => vc.attribute === col.attribute)

      return newValueColumn
        ? {
            ...col,
            ...newValueColumn,
            order: value.indexOf(newValueColumn) + 1,
          }
        : col
    })

    tableSettings.value = { ...tableSettings.value, columns: currentColumns }
    settingsRef.value.customizationRef.save(value)
  },
})

const totalColumns = computed(() => visibleColumns.value.length)

const computedUrlPath = computed(() => {
  if (props.urlPath) {
    return props.urlPath
  }

  return '/' + props.resourceName + '/' + 'table'
})

const selectedRows = computed(() =>
  collection.items.filter(row => row.tSelected)
)

const selectedRowsCount = computed(() => selectedRows.value.length)

const selectedRowsIds = computed(() => selectedRows.value.map(row => row.id))

const hasSelectedRows = computed(() => selectedRowsCount.value > 0)

const allRowsSelected = computed(
  () => selectedRowsCount.value === collection.items.length
)

const isSelectable = computed(() => {
  if (collection.items.length === 0) {
    return false
  }

  if (
    tableSettings.value.hasCustomActions !== null &&
    tableSettings.value.hasCustomActions
  ) {
    return true
  }

  return tableSettings.value.actions.length > 0
})

const {
  queryBuilderRules: rules,
  rulesAreValid,
  hasRulesApplied,
  resetQueryBuilderRules,
} = useQueryBuilder(filtersIdentifier, filtersView)

const {
  hasRules,
  activeFilter,
  filtersBuilderVisible,
  toggleFiltersBuilderVisibility,
} = useFilterable(filtersIdentifier, filtersView)

/**
 * Make new HTTP table request.
 *
 * @param {boolean} viaUserSearch
 */
function request(viaUserSearch = false) {
  if (isLoading.value) {
    return
  }

  cancelPreviousRequest()

  setLoading(true)

  // Reset the current page as the search won't be accurate as there will
  // be offset on the query and if any results are found, won't be queried
  if (viaUserSearch === true && collection.currentPage !== 1) {
    setPage(1)
  }

  let queryStringParams = requestQueryStringParams.value

  if (rulesAreValid.value) {
    queryStringParams.rules = rules.value
  }

  if (search.value) {
    queryStringParams.q = search.value
  }

  Innoclapps.request(computedUrlPath.value, {
    params: queryStringParams,
    cancelToken: new CancelToken(token => (requestCancelToken = token)),
  })
    .then(({ data }) => {
      collection.setState(data)
      configureWatchers()
      emit('loaded', { empty: isEmpty.value })
    })
    .finally(() => {
      setLoading(false)

      if (!initialDataLoaded.value) {
        // Add a little timeout so if there is no record and empty state
        // exists the table is not shown together with the empty state then hidden
        useTimeoutFn(() => (initialDataLoaded.value = true), 150)
      }
    })
}

function configurePolling() {
  clearPollingIntervalId = setInterval(
    request,
    tableSettings.value.pollingInterval * 1000
  )
}

function clearPolling() {
  clearPollingIntervalId && clearInterval(clearPollingIntervalId)
}

/**
 * Prepare the component
 */
function prepareComponent() {
  collection.perPage = parseInt(tableSettings.value.perPage)
  collection.set('order', tableSettings.value.order)

  // Set the watchers after the inital data setup
  // This helps to immediately trigger watcher change|new value before setting the data
  nextTick(() => {
    if (hasRules.value) {
      // Configure the watchers for filters, the filters will update the data
      // and the watchers will catch the change in "requestQueryStringParams" to invoke the request
      configureWatchers()
    } else {
      request()
    }
    componentReady.value = true
  })
}

/**
 * Fetch the current table settings.
 */
async function fetchTableSettings() {
  await fetchSettings(props.resourceName, {
    params: props.dataRequestQueryString,
  })
}

async function refetchActions() {
  await fetchActions(props.resourceName, {
    params: props.dataRequestQueryString,
  })
}

function setPage(page) {
  collection.currentPage = page
}

function registerReloaders() {
  useGlobalEventListener('floating-resource-updated', () => request())

  useGlobalEventListener('action-executed', ({ resourceName }) => {
    if (resourceName === props.resourceName) {
      request()
    }
  })

  useGlobalEventListener('reload-resource-table', id => {
    if (id === props.tableId) {
      request()
    }
  })

  useGlobalEventListener('resource-updated', ({ resourceName }) => {
    if (resourceName === props.resourceName) {
      request()
    }
  })
}

function cancelPreviousRequest() {
  if (!requestCancelToken) {
    return
  }

  requestCancelToken()
}

// eslint-disable-next-line no-unused-vars
function applyFilters(rules) {
  // Wait till Vuex is updated
  nextTick(request)
}

function selectOnRowClick(e, row) {
  if (!isSelectable.value) {
    return
  }

  // Only works when there is at least one row selected
  if (selectedRowsCount.value === 0) {
    return
  }

  if (
    e.target.tagName == 'INPUT' ||
    e.target.tagName == 'SELECT' ||
    e.target.tagName == 'TEXTAREA' ||
    e.target.isContentEditable ||
    e.target.tagName == 'A' ||
    e.target.tagName == 'BUTTON'
  ) {
    return
  }

  onRowSelected(row)
}

function onRowSelected(row) {
  row.tSelected = !row.tSelected
}

function toggleSelectAll() {
  if (allRowsSelected.value) {
    unselectAllRows()
  } else {
    selectAllRows()
  }
}

function unselectAllRows() {
  collection.items.forEach(row => (row.tSelected = false))
}

function selectAllRows() {
  collection.items.forEach(row => (row.tSelected = true))
}

/**
 * Configure the component necessary watched
 */
function configureWatchers() {
  if (watchersInitialized.value === true) {
    return
  }

  watchersInitialized.value = true

  watch(
    () => tableSettings.value.pollingInterval,
    (newVal, oldVal) => {
      if (!oldVal && newVal) {
        configurePolling()
      } else if (!newVal) {
        clearPolling()
      } else if (newVal && oldVal) {
        clearPolling()
        configurePolling()
      }
    },
    { immediate: true }
  )

  watch(
    requestQueryStringParams,
    (newVal, oldVal) => {
      if (!isEqual(newVal, oldVal)) {
        request()
      }
    },
    { deep: true }
  )

  watch(
    () => tableSettings.value.perPage,
    function (newVal) {
      collection.perPage = parseInt(newVal)
    }
  )

  watch(
    () => tableSettings.value.order,
    newVal => {
      // Sometimes when fast switching through tables
      // the order is undefined
      if (newVal) {
        collection.set('order', newVal)
      }
    },
    {
      deep: true,
    }
  )
}

function handleMountedLifeCycle() {
  registerReloaders()
  fetchTableSettings().then(prepareComponent)
}

onMounted(handleMountedLifeCycle)
onBeforeUnmount(cancelPreviousRequest)

onUnmounted(() => {
  // We will check if there is an active filter already applied in store before clearing QB
  // helps keeping the previous filter applied when navigating from the page
  // where the filters are mounted and then going back
  if (!activeFilter.value) {
    resetQueryBuilderRules()
  }
  filtersBuilderVisible.value = false
  collection.flush()
  clearPolling()
  setLoading(false)
})

defineExpose({
  refetchActions,
  setPage,
})
</script>

<style>
th.sortable-chosen.sortable-ghost {
  @apply bg-neutral-200/70 text-neutral-500 dark:bg-neutral-600 dark:text-neutral-200 !important;
}

th.sortable-chosen.sortable-drag {
  @apply border border-dashed border-neutral-400 dark:border-neutral-500;
}
</style>
